package backend;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class PDreamSolver {

	private Position dim;
	private HashMap<Position, Element> map = new HashMap<Position, Element>();
	private PipeContainer[] pipes = new PipeContainer[7];
	private InputTypes direction;
	private InputTypes startDirection;
	private Position acPosition;
	private Position startPosition;
	private Answer bestAnswer = new Answer();
	private Answer currAnswer = new Answer();
	private double timeSpent = 0;
	private int maxLength=0;
	private PipeDreamOutput output;
	private boolean progressive;

	public PDreamSolver(String path, PipeDreamOutput output, boolean progressive) throws IOException, ParserException {
		inicPipes();
		this.progressive = progressive;
		this.output = output;
		FileReader file = new FileReader(path);
		Parser.Parseo(new BufferedReader(file), this);
		//Calculate the length of a solution without blank spaces on the map
		int spaces = dim.getCol() * dim.getRow();
		spaces -= map.size(); // map.size(): the number of walls plus the start pipe
		spaces += pipes[6].cant; // count 2 times for the case the crosses are used 2 times.
		// Calculate the length of a solution using all the available pipes
		int pipesMax=0;
		for (int i = 0; i < pipes.length - 1; i++) {
			pipesMax += pipes[i].cant;
		}
		pipesMax+= pipes[6].cant * 2;
		pipesMax++;
		maxLength = Math.min(spaces, pipesMax);
	}

	public Position getDim() {
		return dim;
	}

	public double getTime() {
		return timeSpent;
	}

	public Position getAcPosition() {
		return acPosition;
	}
	
	public HashMap<Position, Element> getMap() {
		return map;
	}
	
	/* Returns true if the actual position is outside the board */
	private boolean waterIsOut() {
		return (acPosition.getCol() < 0
				|| acPosition.getCol() > dim.getCol() - 1
				|| acPosition.getRow() < 0 || acPosition.getRow() > dim
				.getRow() - 1);
	}
	
	/* Returns true if it is correct to put a pipe in the actual position
	 * OR the actual position is a crossPipe.
	 */
	private boolean canPut() {
		//Element auxElement;
		if (!(map.containsKey(acPosition))) {
			return true;
		}
		
		/*else if ((auxElement = (map.get(acPosition))) instanceof Pipe) {
			return ((Pipe) auxElement).isCrossPipe();
		}
		*/
		return false;
	}
	
	private boolean onCrossPipe(){
		Element auxElement;
		if ((auxElement = (map.get(acPosition))) instanceof Pipe) {
			return ((Pipe) auxElement).isCrossPipe();
		}
		return false;
	}
	/*	
	 * Adds the value Element at the key position.
	 */
	protected void add(Position position, Element element) {
		map.put(position, element);
	}
	
	
	public void setStart(InputTypes n, Position position) {
		this.direction = n;
		this.startDirection = n;
		this.acPosition = position;
		this.startPosition = new Position(position.getY(), position.getX());
	}
	
	/*
	 * Adds the pipe in the container to the map and to the current answer.
	 * Decreases the number of available pipes of the current pipe.
	 * Changes the direction and moves the actual position according to
	 * the pipe used.
	 */
	private void add(PipeContainer container) {
		Position p = new Position(acPosition.getRow(), acPosition.getCol());
		add(p, container.pipe);
		if(progressive){
			output.addPipe(p, container.pipe.directions);
		}
		currAnswer.add(container.pipe, p);
		container.cant--;
		this.direction = container.pipe.outDirection(direction);
		moveToDir(direction);
	}

	/*
	 * Moves the actual position in the direction of the argument.
	 */
	private void moveToDir(InputTypes dir) {
		this.acPosition.setCol(acPosition.getCol()
				+ dir.getDirection().getCol());
		this.acPosition.setRow(acPosition.getRow()
				+ dir.getDirection().getRow());
	}

	/*
	 * Removes the pipe from the map and from the currentAnswer. (It is 
	 * the last added). Also increments the amount of available pipes
	 * of the removed pipe.
	 */
	private void remove(PipeContainer container) {
		map.remove(acPosition);
		if(progressive){
			output.removePipe(acPosition);
		}
		currAnswer.removeLast();
		container.cant++;
	}

	/*
	 * Returns true if the bestAnswer is as long as an answer using all
	 * the pipes available at the beginning.
	 */
	private boolean gotBestAnswer() {
		return maxLength == bestAnswer.pipesCant;
	}

	public void exact() {

		ArrayList<InputTypes> list = new ArrayList<InputTypes>();
		list.add(direction);
		Position pos = new Position(acPosition.getRow(), acPosition.getCol());
		currAnswer.add(new Pipe(list), pos);
		map.put(pos, new Pipe(list));
		moveToDir(direction);

		double startTime = System.currentTimeMillis();
		
		for (int i = 0; i < pipes.length && !gotBestAnswer(); i++) {
			PipeContainer p = pipes[i];
			if (p.hasPipes() && p.pipe.canGo(direction)) {
				exact(p);
			}
		}
		
		double endTime = System.currentTimeMillis();
		timeSpent = (endTime - startTime)/1000;
		output.printSolution(bestAnswer.getPipes());
		return ;
	}

	
	private void exact(PipeContainer container) {

		boolean usedCross = false;
		
		if (waterIsOut()) {
			if (currAnswer.isBetterThan(bestAnswer)) {
				List list = new ArrayList(currAnswer.firstPipe);
				bestAnswer = new Answer(currAnswer.pipesCant, list);
			}
			return ;
		} 
		
		if ((canPut() || onCrossPipe()) && container.hasPipes()) {
			if(onCrossPipe()){
				usedCross = true;
				pipes[6].cant++;
				add(pipes[6]);
			}else{
				add(container);
			}
			
			for (int i = 0; i < pipes.length && (canPut() || onCrossPipe()) && !gotBestAnswer(); i++) {
				PipeContainer p = pipes[i];
				if(!( p.pipe.isCrossPipe() && !isWorthTrying())){
					if (p.pipe.canGo(direction)) {
						exact(p);
					}
				}
			}
			moveToDir(direction.oposite());
			Pipe prev = (Pipe) map.get(acPosition);
			direction = prev.outDirection(direction.oposite()).oposite();

			if (!usedCross) {
				remove(container);
			} else {
				//Position aux = currAnswer.lastPosition();
				//output.removePipe(aux);
				currAnswer.removeLast();
			}
		} 
		
		return ;
	}

	private boolean isWorthTrying() {

		Element E,W,N,S;
		switch(direction){

		case N:
			E=map.get(new Position(acPosition.getCol()+1,acPosition.getRow()));
			W=map.get(new Position(acPosition.getCol()-1,acPosition.getRow()));

			if(pipes[4].cant!=0){
				return (E==null && W==null);
			}
			else{
				return true;
			}
		case S:

			E=map.get(new Position(acPosition.getCol()+1,acPosition.getRow()));
			W=map.get(new Position(acPosition.getCol()-1,acPosition.getRow()));

			if(pipes[4].cant!=0){
				return (E==null && W==null);
			}
			else{
				return true;
			}
		case W:
			N=map.get(new Position(acPosition.getCol(),acPosition.getRow()+1));
			S=map.get(new Position(acPosition.getCol(),acPosition.getRow()-1));
			if(pipes[5].cant !=0){
				return (N==null && S==null);
			}
			else{
				return true;
			}

		case E: 
			N=map.get(new Position(acPosition.getCol(),acPosition.getRow()+1));
			S=map.get(new Position(acPosition.getCol(),acPosition.getRow()-1));
			if(pipes[5].cant !=0){
				return (N==null && S==null);
			}     else{
				return true;
			}

		}
		return true;
	}
	
	public void setTube(int cant, int i) {
		pipes[i].setCant(cant);
	}

	private void inicPipes() {

		ArrayList<InputTypes> dir1 = new ArrayList<InputTypes>();
		dir1.add(InputTypes.W);
		dir1.add(InputTypes.N);
		pipes[0] = new PipeContainer(0, new Pipe(dir1));

		ArrayList<InputTypes> dir2 = new ArrayList<InputTypes>();
		dir2.add(InputTypes.E);
		dir2.add(InputTypes.N);
		pipes[1] = new PipeContainer(0, new Pipe(dir2));

		ArrayList<InputTypes> dir3 = new ArrayList<InputTypes>();
		dir3.add(InputTypes.S);
		dir3.add(InputTypes.E);
		pipes[2] = new PipeContainer(0, new Pipe(dir3));

		ArrayList<InputTypes> dir4 = new ArrayList<InputTypes>();
		dir4.add(InputTypes.W);
		dir4.add(InputTypes.S);
		pipes[3] = new PipeContainer(0, new Pipe(dir4));

		ArrayList<InputTypes> dir5 = new ArrayList<InputTypes>();
		dir5.add(InputTypes.S);
		dir5.add(InputTypes.N);
		pipes[4] = new PipeContainer(0, new Pipe(dir5));

		ArrayList<InputTypes> dir6 = new ArrayList<InputTypes>();
		dir6.add(InputTypes.W);
		dir6.add(InputTypes.E);
		pipes[5] = new PipeContainer(0, new Pipe(dir6));

		ArrayList<InputTypes> dir7 = new ArrayList<InputTypes>();
		dir7.add(InputTypes.W);
		dir7.add(InputTypes.N);
		dir7.add(InputTypes.E);
		dir7.add(InputTypes.S);
		pipes[6] = new PipeContainer(0, new Pipe(dir7));
	}

	public InputTypes getDir() {
		return direction;
	}

	public Answer getBestAnswer() {
		return bestAnswer;
	}

	public void setDim(Position dim) {
		this.dim = dim;
	}

	public InputTypes getStartDir() {
		return this.startDirection;
	}

	public Position getStartPos() {
		return this.startPosition;
	}
		
	public boolean gotAnAnswer(){
		return bestAnswer.pipesCant!=0;
	}
	
	public ArrayList<ApproxAnswer> getAnswers(){
		ArrayList<InputTypes> list = new ArrayList<InputTypes>();
		list.add(direction);
		Position pos = new Position(acPosition.getRow(), acPosition.getCol());
		currAnswer.add(new Pipe(list), pos);
		map.put(pos, new Pipe(list));
		moveToDir(direction);

		ArrayList<ApproxAnswer> resp=new ArrayList<ApproxAnswer>(200);

		for (int i = 0; i < pipes.length && (resp.size()<200); i++) {
			PipeContainer p = pipes[i];
			if (p.hasPipes() && p.pipe.canGo(direction)) {
				getAnswers(p, resp);
			}
		}



		return resp;
	}
	
	public void getAnswers(PipeContainer container, List<ApproxAnswer> l){
		
		boolean usedCross = false;
		Element auxPipe;

		if(l.size()>200)
			return ;
		if (waterIsOut()) {
			
				if(l.size()==0 || currAnswer.getPipesCant()!= l.get(l.size()-1).pipesCant){
					List list = new ArrayList(currAnswer.firstPipe);
					PipeContainer[] aux= new PipeContainer[7];
					for(int i=0; i<aux.length; i++){
						aux[i]=new PipeContainer(pipes[i].cant,pipes[i].pipe);
					}
					l.add(new ApproxAnswer(currAnswer.pipesCant, list, aux));
				}
			return ;
		} else if (canPut() && container.hasPipes()) {
			if ((auxPipe = map.get(acPosition)) instanceof Pipe) {
				if (((Pipe) auxPipe).isCrossPipe()) {
					usedCross = true;
					pipes[6].cant++;
					add(pipes[6]);
				}
			} else {
				add(container);
			}
			for (int i = 0; i < pipes.length && canPut() &&  (l.size()<200); i++) {
				PipeContainer p = pipes[i];
				if (p.pipe.canGo(direction)) {
					getAnswers(p,l);
				}
			}

			moveToDir(direction.oposite());
			Pipe prev = (Pipe) map.get(acPosition);
			direction = prev.outDirection(direction.oposite()).oposite();

			if (!usedCross) {
				remove(container);
			} else {
				currAnswer.removeLast();
			}
		} else {
			return ;
		}
		return ;
		
	}
	
	public void approx(int minutes){
		
		long inic = System.currentTimeMillis();
		List<ApproxAnswer> l= getAnswers();
		for (int i=0; i<l.size();i++){
			int rand= (int)(Math.random()*(l.size()-1));
			ApproxAnswer aux= l.get(i);
			l.set(i, l.get(rand));
			l.set(rand, aux);
		}
		for(ApproxAnswer a: l){
			for(int i=0; i<a.pipesCant;i++){
				System.out.print(a.firstPipe.get(i).pos+""+  a.firstPipe.get(i).pipe+"         ");
			}
			System.out.println();
			for(int i=0;i<7;i++){
			System.out.print(a.pipes[i].cant+"   ");
			}
			System.out.println();
		}
		
		while ((inic- System.currentTimeMillis())/60000 < minutes){
			
		}
		System.out.println("A");
		
	}
}
